#include "GameStreamManager.h"
#include "XComponentBackend.h"
#include "core/utils/LimelightUtils.h"
#include "video/VideoProcessor.h"
#include "audio/AudioProcessor.h"
#include "control/touch_screen/PassThroughController.h"
#include "control/touch_screen/TrackpadEmulator.h"
#include "core/utils/NapiUtils.h"
#include <cstdint>
#include <stdarg.h>
#include <unordered_map>
#include <string>

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN  0x0
#define LOG_TAG     "GameStreamManager"
#include <hilog/log.h>


namespace {
constexpr bool DEBUG = false;
}

napi_status GameStreamManager::exportNapi(napi_env env, napi_value exports) {
    const napi_property_descriptor desc[] = {
        {"on", nullptr, on, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"startConnection", nullptr, startConnection, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"stopConnection", nullptr, stopConnection, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"setTouchHandler", nullptr, setTouchHandler, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"sendKeyEvent", nullptr, sendKeyEvent, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"sendControllerArrivalEvent", nullptr, sendControllerArrivalEvent, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"sendMultiControllerEvent", nullptr, sendMultiControllerEvent, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"preDestroy", nullptr, preDestroy, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    NAPI_RETURN_IF_NOT_OK(napiCallResult, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
    return napi_ok;
}

void GameStreamManager::cleanup() {
    xcomponentBackend = nullptr;
    {
        std::unique_lock l(callbackMapMtx);
        callbackMap.clear();
    }
}

napi_value GameStreamManager::on(napi_env env, napi_callback_info info) {
    SETUP_NAPI_ARGS(env, info, 2);
    DEFINE_NAPI_ARG(std::string, event, env, args[0]);
    using namespace std::string_literals;
    napi_threadsafe_function callback;
    NAPI_RETURN_IF_NOT_OK(
        [&] {
            napi_throw_error(env, "IllegalArgument", "Failed to create callback");
            return nullptr;
        }(),
        napi_create_threadsafe_function(
            env, args[1],
            /*async_resource=*/nullptr, /*async_resource_name=*/NapiUtils::toNapiValue(env, "GameStreamManager"s),
            /*max_queue_size=*/0, /*initial_thread_count=*/1,
            /*thread_finalize_data=*/nullptr, /*thread_finalize_cb=*/nullptr,
            /*context=*/nullptr, NapiUtils::callJsCb, &callback
        )
    );
    {
        std::unique_lock l(callbackMapMtx);
        callbackMap.insert({event, std::make_unique<NapiUtils::ThreadSafeFunction>(callback)});
    }
//     OH_LOG_DEBUG(LOG_APP, "Registered callback for event %{public}s to %{public}d", event.c_str(), reinterpret_cast<uintptr_t>(&callbackMap));
    return nullptr;
}

napi_value GameStreamManager::startConnection(napi_env env, napi_callback_info info) {
    OH_LOG_DEBUG(LOG_APP, "Entering startConnection");
    SETUP_NAPI_ARGS(env, info, 20);
    DEFINE_NAPI_ARG(std::string, address, env, args[0]);
    DEFINE_NAPI_ARG(std::string, appVersion, env, args[1]);
    DEFINE_NAPI_ARG(std::string, gfeVersion, env, args[2]);
    DEFINE_NAPI_ARG(std::string, rtspSessionUrl, env, args[3]);
    DEFINE_NAPI_ARG(int32_t, serverCodecModeSupport, env, args[4]);
    DEFINE_NAPI_ARG(int32_t, width, env, args[5]);
    DEFINE_NAPI_ARG(int32_t, height, env, args[6]);
    DEFINE_NAPI_ARG(int32_t, fps, env, args[7]);
    DEFINE_NAPI_ARG(int32_t, bitrate, env, args[8]);
    DEFINE_NAPI_ARG(int32_t, packetSize, env, args[9]);
    DEFINE_NAPI_ARG(int32_t, streamingRemotely, env, args[10]);
    DEFINE_NAPI_ARG(int32_t, audioConfiguration, env, args[11]);
    DEFINE_NAPI_ARG(int32_t, supportedVideoFormats, env, args[12]);
    DEFINE_NAPI_ARG(int32_t, clientRefreshRateX100, env, args[13]);
    DEFINE_NAPI_ARG(int32_t, encryptionFlags, env, args[14]);
    DEFINE_NAPI_ARG(std::span<uint8_t>, riAesKey, env, args[15]);
    if (riAesKey.size() < sizeof(STREAM_CONFIGURATION().remoteInputAesKey)) {
        OH_LOG_WARN(LOG_APP, "RiAesKey is less than %{public}d bytes %{public}d", sizeof(STREAM_CONFIGURATION().remoteInputAesKey), riAesKey.size());
        return nullptr;
    }
    DEFINE_NAPI_ARG(std::span<uint8_t>, riAesIv, env, args[16]);
    if (riAesIv.size() < sizeof(STREAM_CONFIGURATION().remoteInputAesIv)) {
        OH_LOG_WARN(LOG_APP, "RiAesIv is less than %{public}d bytes %{public}d", sizeof(STREAM_CONFIGURATION().remoteInputAesIv), riAesIv.size());
        return nullptr;
    }
    DEFINE_NAPI_ARG(int32_t, videoCapabilities, env, args[17]);
    DEFINE_NAPI_ARG(int32_t, colorSpace, env, args[18]);
    DEFINE_NAPI_ARG(int32_t, colorRange, env, args[19]);

    stopFlag = false;
    xcomponentBackend = std::make_unique<XComponentBackend>(xcomponentId, nativeWindow);
    videoStats.clear();

    serverInfo = {
        .address = address.c_str(),
        .serverInfoAppVersion = appVersion.c_str(),
        .serverInfoGfeVersion = gfeVersion.c_str(),
        .rtspSessionUrl = rtspSessionUrl.c_str(),
        .serverCodecModeSupport = serverCodecModeSupport,
    };
    streamConfig = {
        .width = width,
        .height = height,
        .fps = fps,
        .bitrate = bitrate,
        .packetSize = packetSize,
        .streamingRemotely = streamingRemotely,
        .audioConfiguration = audioConfiguration,
        .supportedVideoFormats = supportedVideoFormats,
        .clientRefreshRateX100 = clientRefreshRateX100,
        .colorSpace = colorSpace,
        .colorRange = colorRange,
        .encryptionFlags = encryptionFlags,
    };
    memcpy(streamConfig.remoteInputAesKey, riAesKey.data(), sizeof(streamConfig.remoteInputAesKey));
    memcpy(streamConfig.remoteInputAesIv, riAesIv.data(), sizeof(streamConfig.remoteInputAesIv));
    CONNECTION_LISTENER_CALLBACKS connectionListenerCb = {
        .stageStarting = [] (int stage) {
            OH_LOG_INFO(LOG_APP, "stage %{public}s starting", LiGetStageName(stage));
            emitEvent("StageStarting", {std::string(LiGetStageName(stage))});
        },
        .stageComplete = [] (int stage) {
            OH_LOG_INFO(LOG_APP, "stage %{public}s completed", LiGetStageName(stage));
            emitEvent("StageComplete", {std::string(LiGetStageName(stage))});
        },
        .stageFailed = [] (int stage, int errCode) {
            OH_LOG_INFO(LOG_APP, "stage %{public}s failed with code %{public}d", LiGetStageName(stage), errCode);
            emitEvent("StageFailed", {std::string(LiGetStageName(stage)), errCode});
        },
        .connectionStarted = [] {
            OH_LOG_INFO(LOG_APP, "connection started");
            emitEvent("ConnectionStarted", {});
        },
        .connectionTerminated = [] (int errCode) {
            OH_LOG_INFO(LOG_APP, "connection terminated with code %{public}d", errCode);
            emitEvent("ConnectionTerminated", {errCode});
        },
        .logMessage = ENABLE_LIMELOG ? limeLogCb : nullptr,
        .rumble = [] (unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
            // NOTE input range is 0-0xff00, a full 0 call will be issued to stop rumbling
            OH_LOG_DEBUG(LOG_APP, "rumble controller #%{public}d at low %{public}d high %{public}d",
                controllerNumber, lowFreqMotor, highFreqMotor);
            emitEvent("Rumble", {controllerNumber, lowFreqMotor, highFreqMotor});
        },
        .connectionStatusUpdate = [] (int connectionStatus) {
            OH_LOG_INFO(LOG_APP, "connection status updated to %{public}d", connectionStatus);
            emitEvent("ConnectionStatusUpdate", {connectionStatus});
        },
        .setHdrMode = [] (bool hdrEnabled) {
            OH_LOG_INFO(LOG_APP, "set hdr mode to %{public}s", hdrEnabled ? "on" : "off");
            emitEvent("SetHdrMode", {static_cast<int32_t>(hdrEnabled)});
        },
        .rumbleTriggers = [] (uint16_t controllerNumber, uint16_t leftTriggerMotion, uint16_t rightTriggerMotion) {
            // TODO did not test the range of this
            OH_LOG_DEBUG(LOG_APP, "rumble triggers of controller #%{public}d left %{public}d right %{public}d",
                controllerNumber, leftTriggerMotion, rightTriggerMotion);
            emitEvent("RumbleTriggers", {controllerNumber, leftTriggerMotion, rightTriggerMotion});
        },
        .setMotionEventState = [] (uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) {
            OH_LOG_INFO(LOG_APP, "setting motion event controller #%{public}d with motion %{public}d report rate %{public}d",
                controllerNumber, motionType, reportRateHz);
            // TODO wait for controller kit
        },
        .setControllerLED = [] (uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) {
            OH_LOG_INFO(LOG_APP, "setting rgb of controller %{public}d r %{public}d g %{public}d b %{public}d",
                controllerNumber, r, g, b);
            // TODO wait for controller kit
        }
    };
    auto decoderRendererCallbacks = VideoProcessor::LimelightVideoCallbacks::callbacks;
    decoderRendererCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
    auto audioRendererCallbacks = AudioProcessor::LimelightAudioCallbacks::callbacks;
    audioRendererCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
    if (int ret = LiStartConnection(
                &serverInfo, &streamConfig,
                &connectionListenerCb, &decoderRendererCallbacks, &audioRendererCallbacks,
                /*renderContext=*/nullptr, /*drFlags=*/0, /*audioContext=*/nullptr, /*arFlags=*/0);
            ret != 0) {
        OH_LOG_INFO(LOG_APP, "Failed to start limelight connection %{public}d", ret);
        return nullptr;
    }

    OH_LOG_INFO(LOG_APP, "Limelight connection has started");

    updateVideoStatsThread = std::thread(updateVideoStatsHandler);

    return nullptr;
}

napi_value GameStreamManager::stopConnection(napi_env env, napi_callback_info info) {
    LiStopConnection();
    OH_LOG_INFO(LOG_APP, "Requested stop limelight connection");
    stopFlag = true;
    stopCond.notify_all();
    if (updateVideoStatsThread.joinable()) {
        updateVideoStatsThread.join();
    }
    xcomponentBackend = nullptr;
    return nullptr;
}

napi_value GameStreamManager::setTouchHandler(napi_env env, napi_callback_info info) {
    SETUP_NAPI_ARGS(env, info, 2);
    DEFINE_NAPI_ARG(std::string, mode, env, args[0]);
    DEFINE_NAPI_ARG(std::string, configStr, env, args[1]);
    const auto config = [&] {
        AbstractTouchScreenControlHandler::Config config;
        std::stringstream ss(configStr);
        for (std::string kvStr; std::getline(ss, kvStr, ','); ) {
            std::stringstream ss(kvStr);
            std::string key, val;
            if (!std::getline(ss, key, ':')) {
                OH_LOG_WARN(LOG_APP, "Failed to extract key from %{public}s", kvStr.c_str());
                continue;
            }
            if (!std::getline(ss, val, ':')) {
                OH_LOG_WARN(LOG_APP, "Failed to extract key from %{public}s", kvStr.c_str());
                continue;
            }
            config.insert({key, val});
        }
        return config;
    }();
    if (!xcomponentBackend) {
        OH_LOG_WARN(LOG_APP, "XComponentBackend is not ready at the time of setTouchHandler");
        napi_throw_error(env, "RuntimeError", "XComponentBackend is not ready");
        return nullptr;
    }
    if (mode == "Touch") {
        xcomponentBackend->setTouchHandler(std::make_unique<PassThroughController>());
    } else if (mode == "Trackpad") {
        xcomponentBackend->setTouchHandler(std::make_unique<TrackpadEmulator>(config));
    } else {
        OH_LOG_WARN(LOG_APP, "Unknown touch control mode %{public}s, ignoring", mode.c_str());
    }
    return nullptr;
}

napi_value GameStreamManager::sendKeyEvent(napi_env env, napi_callback_info info) {
    SETUP_NAPI_ARGS(env, info, 2);
    DEFINE_NAPI_ARG(int32_t, keyCode, env, args[0]);
    DEFINE_NAPI_ARG(int32_t, keyAction, env, args[1]);
    LI_CALL_OR_ELSE(SendKeyboardEvent(keyCode, keyAction, 0x0), return nullptr);
    return nullptr;
}

napi_value GameStreamManager::sendControllerArrivalEvent(napi_env env, napi_callback_info info) {
    SETUP_NAPI_ARGS(env, info, 5);
    DEFINE_NAPI_ARG(uint32_t, controllerNumber, env, args[0]);
    DEFINE_NAPI_ARG(uint32_t, activeGamepadMask, env, args[1]);
    DEFINE_NAPI_ARG(uint32_t, type, env, args[2]);
    DEFINE_NAPI_ARG(uint32_t, supportedButtonFlags, env, args[3]);
    DEFINE_NAPI_ARG(uint32_t, capabilities, env, args[4]);
    LI_CALL_OR_ELSE(SendControllerArrivalEvent(
        controllerNumber, activeGamepadMask,
        type, supportedButtonFlags, capabilities
    ), return nullptr);
    return nullptr;
}

napi_value GameStreamManager::sendMultiControllerEvent(napi_env env, napi_callback_info info) {
    SETUP_NAPI_ARGS(env, info, 9);
    DEFINE_NAPI_ARG(uint32_t, controllerNumber, env, args[0]);
    DEFINE_NAPI_ARG(uint32_t, activeGamepadMask, env, args[1]);
    DEFINE_NAPI_ARG(uint32_t, buttonFlags, env, args[2]);
    DEFINE_NAPI_ARG(uint32_t, leftTrigger, env, args[3]);
    DEFINE_NAPI_ARG(uint32_t, rightTrigger, env, args[4]);
    DEFINE_NAPI_ARG(uint32_t, leftStickX, env, args[5]);
    DEFINE_NAPI_ARG(uint32_t, leftStickY, env, args[6]);
    DEFINE_NAPI_ARG(uint32_t, rightStickX, env, args[7]);
    DEFINE_NAPI_ARG(uint32_t, rightStickY, env, args[8]);
    LI_CALL_OR_ELSE(SendMultiControllerEvent(
        controllerNumber, activeGamepadMask,
        buttonFlags, leftTrigger, rightTrigger,
        leftStickX, leftStickY, rightStickX, rightStickY
    ), return nullptr);
    return nullptr;
}

napi_value GameStreamManager::preDestroy(napi_env env, napi_callback_info info) {
    ENSURE_GSMGR_OR_ELSE(return nullptr);
    xcomponentBackend = nullptr;
    return nullptr;
}

void GameStreamManager::emitEvent(const std::string &event, std::initializer_list<NapiUtils::NapiValueType> args) {
    std::unique_lock l(callbackMapMtx);
    if (const auto cbit = callbackMap.find(event); cbit != callbackMap.cend()) {
        if constexpr (DEBUG) {
            OH_LOG_DEBUG(LOG_APP, "Calling callback %{public}s", cbit->first.c_str());
        }
        (void) [&] { NAPI_RETURN_IF_NOT_OK(napiCallResult, (*cbit->second)(new NapiUtils::ThreadSafeFunction::Arguments(args))); return napi_ok; }();
    } else {
        OH_LOG_INFO(LOG_APP, "Event %{public}s has no associated callback from %{public}d", event.c_str(), reinterpret_cast<uintptr_t>(&callbackMap));
    }
}

void GameStreamManager::limeLogCb(const char *fmt, ...) {
    const auto buf = std::make_unique<char[]>(8192);
    va_list va;
    va_start(va, fmt);
    std::vsnprintf(buf.get(), 8192, fmt, va);
    va_end(va);
    OH_LOG_DEBUG(LOG_APP, "==== LimeLog ==== %{public}s", buf.get());
}

namespace {
napi_value videoStatusToNapiValue(napi_env env) {
    return GameStreamManager::videoStats.toNapiValue(env);
}
}

void GameStreamManager::updateVideoStatsHandler() {
    while (true) {
        using namespace std::chrono_literals;
        std::mutex mtx;
        std::unique_lock l(mtx);
        if (stopCond.wait_for(l, 1s, [&] { return stopFlag.load(); })) {
            break;
        }
        emitEvent("UpdateVideoStats", {videoStatusToNapiValue});
    }
}
